#!/usr/bin/env bash
# vim: fdm=marker fmr=[[[,]]] cms=\ #%s

usage()
{ #[[[
    cat <<EOF

用法: 根据文件生成实体
        $(basename $0) [-o output_dir] [table1.sql] [table2.sql] ...
注意：注释内不要出现', ;'，或'unique key primary'等关键字"
      每行只能有一个注释

用法: 根据数据库配置生成实体，不指定表，则生成库中所有表的实体
        $(basename $0) -d "user/password@host:port/database" [-o output_dir] [table1] [table2] ...
        如：dbuser/dbuser@localhost:3306/data

EOF
} #]]]

init_output()
{ #[[[
    output_dir=$1

    output_dir=${output_dir:-_output}
    if [[ -e $output_dir ]]
    then
        if [[ ! -d $output_dir ]]
        then
            echo "output dir '$output_dir' not DIRECTORY, abort"
            return 1
        fi
        read -p "output dir '$output_dir' exist, DELETE and RECREATE? (y/n) " opt
        opt=${opt^^}
        if [[ $opt != Y && $opt != YES ]]
        then
            echo "Quit..."
            return 1
        fi
    fi
    rm -fr $output_dir
    if (( $? != 0 ))
    then
        echo "output dir '$output_dir' cannot DELETE, abort"
        return 1
    fi
    mkdir $output_dir
    OUTPUT_DIR=$output_dir
} #]]]

init_var()
{ #[[[
    TABLE=''
    ENTITY=''
    UKS=()
    PK=''
    COLS=()
    ID_COLS=()
} #]]]

generate_entity()
{ #[[[
    # primary key数组
    local -a pk_arr
    IFS=, read -a pk_arr <<< "$PK"

    # 实体文件
    local entity_file="${OUTPUT_DIR}/${ENTITY}.java"
    touch $entity_file

    cat >>$entity_file <<EOF
package test;

import java.util.Date;

import javax.persistence.Basic;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.IdClass;
import javax.persistence.Lob;
import javax.persistence.Table;
import javax.persistence.Temporal;
import javax.persistence.TemporalType;
import javax.persistence.UniqueConstraint;

@Entity
EOF
    if (( ${#UKS[@]} > 0 ))
    then
        echo "@Table(name = \"$TABLE\", uniqueConstraints = {" >>$entity_file
        local -a ucs
        for uk in "${UKS[@]}"
        do
            local -a arr
            IFS=, read -a arr <<< $uk
            local cns=''
            local cn
            for cn in ${arr[@]}
            do
                cns+="\"$cn\", "
            done
            cns=${cns:0:-2}
            ucs[${#ucs[@]}]="@UniqueConstraint(columnNames = { $cns })"
        done
        local i
        for i in ${!ucs[@]}
        do
            echo -n "        ${ucs[$i]}" >>$entity_file
            if (( i < ${#ucs[@]}-1 ))
            then
                echo ", " >>$entity_file
            fi
        done
        echo " })" >>$entity_file
    else
        echo "@Table(name = \"$TABLE\")" >>$entity_file
    fi
    if (( ${#pk_arr[@]} > 1 ))
    then
        echo "@IdClass(${ENTITY}Id.class)" >>$entity_file
    fi
    cat >>$entity_file <<EOF
public class $ENTITY
{
EOF

    local -a entity_cols
    local col
    for col in "${COLS[@]}"
    do
        local cname
        local ctype
        local crest
        IFS=" " read cname ctype crest <<< $col
#        echo "    [$cname] [$ctype] [$crest]"

        local -a annotations=() 
        local pk_idx=-1
        local idx
        for idx in ${!pk_arr[@]}
        do
            local pk_col=${pk_arr[$idx]}
            reg='^ *(.*) *$'
            [[ $pk_col =~ $reg ]] && pk_col=${BASH_REMATCH[1]}
            if [[ $cname == $pk_col ]]
            then
                pk_idx=$idx
                annotations[${#annotations[@]}]="@Id"
                if [[ $crest =~ auto_increment ]]
                then
                    annotations[${#annotations[@]}]="@GeneratedValue(strategy = GenerationType.IDENTITY)"
                fi
            fi
        done

        reg='not null'
        local -a column_opt=()
        if [[ $crest =~ $reg ]]
        then
            column_opt[${#column_opt[@]}]="nullable = false"
        else
            column_opt[${#column_opt[@]}]="nullable = true"
        fi

        local jtype=''
        local length

        if [[ $ctype =~ ^bigint ]]
        then
            jtype=long
        elif [[ $ctype == date ]]
        then
            jtype=Date
            annotations[${#annotations[@]}]="@Temporal(TemporalType.DATE)"
        elif [[ $ctype == datetime ]]
        then
            jtype=Date
            annotations[${#annotations[@]}]="@Temporal(TemporalType.TIMESTAMP)"
        elif [[ $ctype =~ ^varchar ]]
        then
            jtype=String
            length=${ctype##*(}
            length=${length%%)*}
            column_opt[${#column_opt[@]}]="length = $length"
        elif [[ $ctype =~ ^char ]]
        then
            jtype=String
            length=${ctype##*(}
            length=${length%%)*}
            column_opt[${#column_opt[@]}]="length = $length"
            column_opt[${#column_opt[@]}]='columnDefinition = "char"'
        elif [[ $ctype =~ ^int ]]
        then
            jtype=int
        elif [[ $ctype =~ ^mediumint ]]
        then
            jtype=int
        elif [[ $ctype =~ ^smallint ]]
        then
            jtype=int
            column_opt[${#column_opt[@]}]='columnDefinition = "smallint"'
        elif [[ $ctype =~ ^tinyint ]]
        then
            jtype=int
            length=${ctype##*(}
            length=${length%%)*}
            if (( length == 1 ))
            then
                column_opt[${#column_opt[@]}]='columnDefinition = "bit"'
            else
                column_opt[${#column_opt[@]}]='columnDefinition = "tinyint"'
            fi
        elif [[ $ctype =~ ^float ]]
        then
            jtype=float
        elif [[ $ctype =~ ^double ]]
        then
            jtype=double
        elif [[ $ctype =~ blob$ ]]
        then
            jtype='byte[]'
            annotations[${#annotations[@]}]='@Lob'
            annotations[${#annotations[@]}]='@Basic(fetch = FetchType.LAZY)'
            if [[ $ctype != longblob ]]
            then
                column_opt[${#column_opt[@]}]='columnDefinition = "'$ctype'"'
            fi
        elif [[ $ctype =~ text$ ]]
        then
            jtype='String'
            annotations[${#annotations[@]}]='@Lob'
            annotations[${#annotations[@]}]='@Basic(fetch = FetchType.LAZY)'
            if [[ $ctype != longtext ]]
            then
                column_opt[${#column_opt[@]}]='columnDefinition = "'$ctype'"'
            fi
        else
            echo "Column type($ctype) not supported"
            return 1
        fi

        if [[ ${#column_opt[@]} > 0 ]]
        then
            local opts=''
            local opt
            for opt in "${column_opt[@]}"
            do
                opts+="${opt}, "
            done
            opts=${opts:0:-2}

            annotations[${#annotations[@]}]="@Column($opts)"
        fi

        local annotation
        for annotation in "${annotations[@]}"
        do
            echo "    $annotation" >>$entity_file
        done
        echo "    private $jtype $cname;" >>$entity_file
        echo >>$entity_file

        entity_cols[${#entity_cols[@]}]="$cname,$jtype"
        if (( pk_idx >= 0 ))
        then
            ID_COLS[$pk_idx]="$cname,$jtype"
        fi
    done

    for line in "${entity_cols[@]}"
    do
        IFS=, read jname jtype <<< $line
        jname_f=${jname:0:1}
        jname_f=${jname_f^^}${jname:1}
        cat >>$entity_file <<EOF
    public $jtype get${jname_f}()
    {
        return $jname;
    }

    public void set${jname_f}($jtype $jname)
    {
        this.${jname} = $jname;
    }

EOF
    done

    cat >>$entity_file <<EOF
}
EOF
} #]]]

generate_id()
{ #[[[
    # primary key数组
    local -a pk_arr
    IFS=, read -a pk_arr <<< "$PK"
    if (( ${#pk_arr[@]} <= 1 ))
    then
        return
    fi

    local id_file="${OUTPUT_DIR}/${ENTITY}Id.java"
    touch $id_file

    cat >>$id_file <<EOF
package test;

import java.io.Serializable;
import java.util.Date;

@SuppressWarnings("serial")
public class ${ENTITY}Id implements Serializable
{
EOF

    local line
    local jname
    local jtype

    for line in "${ID_COLS[@]}"
    do
        IFS=, read jname jtype <<< $line
        cat >>$id_file <<EOF
    private $jtype $jname;

EOF
    done

    cat >>$id_file <<EOF
    public ${ENTITY}Id()
    {
    }

EOF

    for line in "${ID_COLS[@]}"
    do
        IFS=, read jname jtype <<< $line
        local jname_f=${jname:0:1}
        local jname_f=${jname_f^^}${jname:1}
        cat >>$id_file <<EOF
    public $jtype get${jname_f}()
    {
        return $jname;
    }

    public void set${jname_f}($jtype $jname)
    {
        this.${jname} = $jname;
    }

EOF
    done

    cat >>$id_file <<EOF
    @Override
    public int hashCode()
    {
        final int prime = 31; 
        int result = 1;
EOF
    for line in "${ID_COLS[@]}"
    do
        IFS=, read jname jtype <<< $line

        if [[ $jtype == "int" ]]
        then
            cat >>$id_file <<EOF
        result = prime * result + ${jname};
EOF
        elif [[ $jtype == "long" ]]
        then
            cat >>$id_file <<EOF
        result = prime * result
                + (int) (${jname} ^ (${jname} >>> 32));
EOF
        elif [[ $jtype == "float" ]]
        then
            cat >>$id_file <<EOF
        result = prime * result
                + Float.floatToIntBits(${jname});
EOF
        elif [[ $jtype == "double" ]]
        then
            cat >>$id_file <<EOF
        long ${jname}_temp = Double.doubleToLongBits(${jname});
        result = prime * result
                + (int) (${jname}_temp ^ (${jname}_temp >>> 32));
EOF
        else
            cat >>$id_file <<EOF
        result = prime * result
                + ((${jname} == null) ? 0 : ${jname}.hashCode());
EOF
        fi
    done
    cat >>$id_file <<EOF
        return result;
    }

EOF

    cat >>$id_file <<EOF
    @Override
    public boolean equals(Object obj)
    {   
        if (this == obj)
            return true;
        if (obj == null)
            return false;
        if (getClass() != obj.getClass())
            return false;
        ${ENTITY}Id other = (${ENTITY}Id) obj;
EOF
    for line in "${ID_COLS[@]}"
    do
        IFS=, read jname jtype <<< $line

        if [[ $jtype == "int" || $jtype == "long" ]]
        then
            cat >>$id_file <<EOF
        if (${jname} != other.${jname})
            return false;
EOF
        elif [[ $jtype == "float" ]]
        then
            cat >>$id_file <<EOF
        if (Float.floatToIntBits(${jname})
                != Float.floatToIntBits(other.${jname}))
            return false;
EOF
        elif [[ $jtype == "double" ]]
        then
            cat >>$id_file <<EOF
        if (Double.doubleToLongBits(${jname})
                != Double.doubleToLongBits(other.${jname}))
            return false;
EOF
        else
            cat >>$id_file <<EOF
        if (${jname} == null)
        {
            if (other.${jname} != null)
                return false;
        }
        else if (!${jname}.equals(other.${jname}))
            return false;
EOF
        fi
    done

    cat >>$id_file <<EOF
        return true;
    }

}
EOF
} #]]]

generate()
{ #[[[
    local sql=$1
#    echo "$sql"

    init_var

    # 解析
    local reg='CREATE *TABLE(.*)'
    if [[ "$sql" =~  $reg ]]    # 必须用变量
    then
        sql="${BASH_REMATCH[1]}"
#        echo "$sql"
    else
        echo "no 'CREATE TABLE' in $sql"
        return 1
    fi

    # if not exists TABLE (BODY) OPTION
    sql=${sql%%;*}
#    echo "$sql"

    TABLE=${sql%%(*}      # if not exists TABLE
#    echo "$TABLE"
    reg='([^ ]*) *$'
    if [[ "$TABLE" =~  $reg ]]
    then
        TABLE="${BASH_REMATCH[1]}"
#        echo "$TABLE"
    else
        echo "$sql invalid"
        return 1
    fi

    # BODY
    local body=${sql#*(}
    body=${body%)*}
#    echo "$body"

    reg='PRIMARY *KEY'
    if [[ "$body" =~ $reg ]]
    then
        echo
    else
        echo "no 'PRIMARY KEY' in $body"
        return 1
    fi
    local c1=''
    local c2=''
    reg='(UNIQUE.*)$'
    if [[ "$body" =~ $reg ]]
    then
        c1="${BASH_REMATCH[1]}"
    fi
    reg='(PRIMARY *KEY.*)$'
    if [[ "$body" =~ $reg ]]
    then
        c2="${BASH_REMATCH[1]}"
    fi
    if (( ${#c1} > ${#c2} ))
    then
        local keys_block=$c1
    else
        local keys_block=$c2
    fi
    local cols_block=${body:0:-${#keys_block}}
#    echo "$cols_block"
#    echo "$keys_block"
#    echo

    # Primary Key & Unique Key
    local -a lines
    IFS=! read -a lines <<< "$keys_block"

    local line
    for line in "${lines[@]}"
    do
        line=${line/\/\**\*\/}      # 去除 /* */，但只能一个

        reg='PRIMARY *KEY'
        if [[ $line =~ $reg ]]
        then
            PK=${line##*(}
            PK=${PK%%)*}
            continue
        fi

        reg='UNIQUE *'
        if [[ $line =~ $reg ]]
        then
            local uk=${line##*(}
            uk=${uk%%)*}
            UKS[${#UKS[@]}]=$uk
            continue
        fi
    done

    # Columns
    IFS=! read -a lines <<< "$cols_block"
    for line in "${lines[@]}"
    do
        line=${line/\/\**\*\/}      # 去除 /* */，但只能一个
        reg='[^ ]+'
        if [[ $line =~ $reg ]]
        then
            COLS[${#COLS[@]}]=$line
        fi
    done
    echo

    echo "table: $TABLE"
    echo "primary key: $PK"
    echo "unique key:"
    local uk
    for uk in "${UKS[@]}"
    do
        echo "    $uk"
    done
    echo "columns:"
    local col
    for col in "${COLS[@]}"
    do
        echo "    $col"
    done
    echo

    local -a arr
    IFS=_ read -a arr <<< $TABLE
    ENTITY=''
    local a
    for a in ${arr[@]}
    do
        local f=${a:0:1}
        ENTITY+=${f^^}${a:1}
    done

    # 实体文件
    generate_entity

    # 复合Id文件
    generate_id
} #]]]

do_db()
{ #[[[
    local split="--------------------------------------------------"

    local db_config=$1
    shift

    local -a arr
    IFS=@ read -a arr <<<$db_config
    local user_password=${arr[0]}
    local host_port_database=${arr[1]}
    IFS=/ read -a arr <<<$user_password
    local user=${arr[0]}
    local password=${arr[1]}
    IFS=/ read -a arr <<<$host_port_database
    local host_port=${arr[0]}
    local database=${arr[1]}
    IFS=: read -a arr <<<$host_port
    local host=${arr[0]}
    local port=${arr[1]}
    [[ -z $port ]] && port=3306

    local opts="-r -s -h $host -P $port -u$user -p$password $database"
#   local opts="-d --skip-add-drop-table -h $host -P $port -u$user -p$password $database"
    local tables
    if (( $# == 0 ))
    then
        tables=$(mysql $opts <<< "show tables;")
    else
        tables="$@"
    fi

    local table
    for table in $tables
    do
        echo "Processing $table ..."
        echo

#    local sql=$(mysqldump $opts $table)
        local sql=$(mysql $opts <<< "show create table $table;")
        sql=$(echo "$sql" | sed -n '
    s/\t/ /g
    s/^ *--.*$//
    s/^ *\/\*.*$//
    s/`//g
    s/'\''[^'\'']*'\''/'\'\''/g
    s/, *$/!/
    s/^ *key .*$//
    H
    $ {
        g
        s/\n//g
        p
    }
')
        generate "$sql"
        if (( $? != 0 ))
        then
            echo "Fail, abort ..."
            return
        fi

        echo $split
    done
} #]]]

do_file()
{ #[[[
    local split="--------------------------------------------------"

    while (( $# > 0 ))
    do
        local sql_file=$1

        echo "Processing $sql_file ..."
        echo

        local sql=$(sed -n '
    s/\t/ /g
    s/^ *--.*$//
    s/^ *\/\*.*$//
    s/`//g
    s/'\''[^'\'']*'\''/'\'\''/g
    s/, *$/!/
    s/^ *key .*$//
    H
    $ {
        g
        s/\n//g
        p
    }
' $sql_file)
        generate "$sql"
        if (( $? != 0 ))
        then
            echo "Fail, abort ..."
            return
        fi

        echo $split

        shift
    done
} #]]]

main()
{ #[[[
    local split="=================================================="

    while getopts 'd:o:h' opt
    do
        case $opt in
            d)
                local db_config=$OPTARG
                ;;
            o)
                local output_dir=$OPTARG
                ;;
            *)
                usage
                return
                ;;
        esac
    done
    shift $((OPTIND-1))
    if [[ -z $db_config ]]
    then
        if (( $# == 0 ))
        then
            usage
            return
        fi
    fi
    echo

    init_output "$output_dir"
    if (( $? != 0 ))
    then
        return
    fi
    echo $split

    if [[ ! -z $db_config ]]
    then
        do_db $db_config "$@"
    else
        do_file "$@"
    fi
} #]]]

shopt -s nocasematch
main "$@"
